from binary_agent import binary_agent
from packetholder import packetholder
from constants import PCMD, PSIZE, PRIO, LSTATE, CCONSTS, SYSTEM
from collections import deque
import random

class cachereq(object):
  # line is the key for the list that holds these, so...
  req_type = None

  # these are filled in by process() as needed
  fulfilled = False
  owner = None
  start_time = None

  def __init__(self, new_type=None):
    self.req_type = new_type
    self.start_time = SYSTEM.TIME

class cache(binary_agent):
  memtable = None

  size_l = None
  data = None

  active = True

  active_requests = None
  sleeping_requests = None

  dma_node = None
  dma_request_pending = False
  dma_start_time = False

  stat_resolved_requests = 0
  stat_resolved_dmas = 0
  
  stat_accum_req_time = 0
  stat_accum_dma_time = 0
  stat_max_req_time = 0
  stat_max_dma_time = 0

  def __init__(self, mtable):
    super(cache, self).__init__()

    if CCONSTS.MAX_REQUESTS > CCONSTS.NUM_LINES:
      raise NameError("cache: CCONSTS.MAX_REQUESTS <= CCONSTS.NUM_LINES required")
    
    for holder in self.in_holders:
      holder.max_packets = CCONSTS.MAX_PACKETS
    for holder in self.out_holders:
      holder.max_packets = CCONSTS.MAX_PACKETS

    self.memtable = mtable

    self.size_l = CCONSTS.NUM_LINES
    self.data = {}

    self.active_requests = {}
    self.sleeping_requests = {}

  def count_requests(self):
    return len(self.active_requests) + len(self.sleeping_requests)

  def handle_packet(self, pkt):
    # wakeup sleeping request as needed
    if pkt.desc in self.sleeping_requests:
      self.active_requests[pkt.desc] = self.sleeping_requests.pop(pkt.desc)

    if(pkt.cmd is PCMD.REP_READ):
      if (pkt.desc not in self.data) and (len(self.data) >= self.size_l):
        raise NameError("cache: No space to handle REP_READ")
        # this should never happen if the state machines are working right
      self.data[pkt.desc] = LSTATE.S
      return True

    elif(pkt.cmd is PCMD.REQ_INVL):
      # request from directroy to invalidate this entry
      lstate = self.data.get(pkt.desc, LSTATE.I)
      # do not bother invalidating already invalid data
      if lstate is LSTATE.SP:
        self.data[pkt.desc] = LSTATE.IP
      elif lstate not in [LSTATE.I, LSTATE.IP]:
        self.data.pop(pkt.desc)

      # issue response
      psize = PSIZE.ACK_INVL
      if(lstate is LSTATE.M): psize += SYSTEM.FLIT_P_LINE

      new_pkt = self.issue_packet(psize, PRIO.ACK_INVL)
      if new_pkt is None: return False

      new_pkt.dest = pkt.src
      new_pkt.cmd = PCMD.ACK_INVL
      new_pkt.desc = pkt.desc
      new_pkt.age = pkt.age
      return True

    elif(pkt.cmd is PCMD.REP_EXCL):
      if (pkt.desc not in self.data) and (len(self.data) >= self.size_l):
        raise NameError("cache: No space to handle REP_EXCL")
        # this should never happen if the state machines are working right
      self.data[pkt.desc] = LSTATE.M
      return True

    elif(pkt.cmd is PCMD.DMA_DONE):
      if self.dma_request_pending:
        self.dma_request_pending = False
        dma_time = SYSTEM.TIME - self.dma_start_time;

        if dma_time > self.stat_max_dma_time: self.stat_max_dma_time = dma_time
        self.stat_accum_dma_time += dma_time
        self.stat_resolved_dmas += 1

      else:
        raise NameError("cache: Lost track of DMA request")
      return True
        
    else:
      raise NameError("cache: Bad Packet CMD")
      return True

  def take_read_request(self, line):
    if(self.count_requests() >= CCONSTS.MAX_REQUESTS):
      return False
    elif (line in self.active_requests) or (line in self.sleeping_requests):
      return False

    self.active_requests[line] = cachereq(LSTATE.S)

    return True

  def take_write_request(self, line):
    if(self.count_requests() >= CCONSTS.MAX_REQUESTS):
      return False
    elif (line in self.active_requests) or (line in self.sleeping_requests):
      return False

    self.active_requests[line] = cachereq(LSTATE.M)

    return True

  def process(self):
    super(cache, self).process()

    if self.active:
      if (not self.dma_request_pending) and self.dma_node is not None:
        new_pkt = self.issue_packet(PSIZE.DMA_READ, PRIO.DMA_READ)
        if new_pkt is None: return

        dma_length = SYSTEM.LINE_P_DMA
        dma_line = max(0, self.memtable.get_random_l()-dma_length)

        new_pkt.age = SYSTEM.TIME
        new_pkt.dest = self.dma_node
        if random.randint(1,2) is 1:
          new_pkt.cmd = PCMD.DMA_READ
        else:
          new_pkt.cmd = PCMD.DMA_WRITE
        new_pkt.desc = dma_line
        new_pkt.extra = dma_length

        self.dma_request_pending = True
        self.dma_start_time = SYSTEM.TIME
        return
      elif(self.count_requests() < CCONSTS.MAX_REQUESTS):
        if random.randint(1,10) is 1:
          # 10% of the time issue a write request 
          if self.data and (random.randint(1,2) is 1):
            # half of write requests to data already got
            self.take_write_request(random.choice(self.data.keys()))
          else:
            self.take_write_request(self.memtable.get_random_l())
        else:
          # 90% of the time issue a read request
          self.take_read_request(self.memtable.get_random_l())
    
    for req_line, request in self.active_requests.items():
      lstate = self.data.get(req_line, LSTATE.I)
      
      # invalidate a cache entry to get space as needed 
      if (lstate is LSTATE.I) and (len(self.data) >= self.size_l):
        # no space, so invalidate some cache line
        for cline in self.data.keys():
          if not (cline in self.active_requests or cline in self.sleeping_requests):
            dropping = cline
        dstate = self.data[dropping]

        psize = PSIZE.ACK_INVL
        if(dstate is LSTATE.M): psize += SYSTEM.FLIT_P_LINE

        new_pkt = self.issue_packet(psize, PRIO.ACK_INVL)
        if new_pkt is not None:
          new_pkt.age = SYSTEM.TIME
          new_pkt.dest = self.memtable.lookup(dropping)
          new_pkt.cmd = PCMD.ACK_INVL
          new_pkt.desc = dropping
          del self.data[dropping]

          return # only issue 1 packet per cycle

      else:
        if request.req_type is LSTATE.S:
          if lstate is LSTATE.I:
            # craft request to elevate to S
            new_pkt = self.issue_packet(PSIZE.REQ_READ, PRIO.REQ_READ)
            if new_pkt is not None:
              request.owner = self.memtable.lookup(req_line)

              new_pkt.age = SYSTEM.TIME
              new_pkt.dest = request.owner
              new_pkt.cmd = PCMD.REQ_READ
              new_pkt.desc = req_line
              self.data[req_line] = LSTATE.IP

              # sleep this request
              self.sleeping_requests[req_line] = self.active_requests.pop(req_line)
              return # only issue 1 packet per cycle

          elif lstate in [LSTATE.S, LSTATE.SP, LSTATE.M]:
            # request resolved
            if not request.fulfilled:
              req_time = SYSTEM.TIME - request.start_time

              if req_time > self.stat_max_req_time: self.stat_max_req_time = req_time
              self.stat_accum_req_time += req_time
              self.stat_resolved_requests += 1
              
              request.fulfilled = True

            # check if must send acknowledgement
            if request.owner:
              new_pkt = self.issue_packet(PSIZE.REQ_DONE, PRIO.REQ_DONE)
              if new_pkt:
                new_pkt.age = SYSTEM.TIME
                new_pkt.dest = request.owner
                new_pkt.cmd = PCMD.REQ_DONE
                new_pkt.desc = req_line
                request.owner = None # mark request as deletable

                self.active_requests.pop(req_line)
            else:
              self.active_requests.pop(req_line)

        else:
          # only other possible request is to write
          if lstate in [LSTATE.I, LSTATE.S]:
            # craft request to elevate to M

            new_pkt = self.issue_packet(PSIZE.REQ_EXCL, PRIO.REQ_EXCL)
            if new_pkt is not None:
              request.owner = self.memtable.lookup(req_line)

              new_pkt.age = SYSTEM.TIME
              new_pkt.dest = request.owner
              new_pkt.cmd = PCMD.REQ_EXCL
              new_pkt.desc = req_line
              if lstate is LSTATE.S:
                self.data[req_line] = LSTATE.SP
              else:
                self.data[req_line] = LSTATE.IP
                new_pkt.extra = 1

              # sleep this request
              self.sleeping_requests[req_line] = self.active_requests.pop(req_line)
              return # only issue 1 packet per cycle
        
          elif lstate is LSTATE.M:
            # request always resolved by now
            if not request.fulfilled:
              req_time = SYSTEM.TIME - request.start_time

              if req_time > self.stat_max_req_time: self.stat_max_req_time = req_time
              self.stat_accum_req_time += req_time
              self.stat_resolved_requests += 1
              
              request.fulfilled = True

            # check if must send acknowledgement
            if request.owner:
              new_pkt = self.issue_packet(PSIZE.REQ_DONE, PRIO.REQ_DONE)
              if new_pkt:
                new_pkt.age = SYSTEM.TIME
                new_pkt.dest = request.owner
                new_pkt.cmd = PCMD.REQ_DONE
                new_pkt.desc = req_line

                self.active_requests.pop(req_line)
            else: 
              self.active_requests.pop(req_line)

  def to_string(self):
    return str(self.stat_resolved_requests + self.stat_resolved_dmas)
